/*
 * Copyright 2009 Andreas Borglin andreas.borglin@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package borge.mobile.lib.pimp;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Vector;
import javax.microedition.amms.GlobalManager;
import javax.microedition.amms.MediaProcessor;
import javax.microedition.amms.MediaProcessorListener;
import javax.microedition.amms.control.ImageFormatControl;
import javax.microedition.amms.control.imageeffect.*;
import javax.microedition.io.Connector;
import javax.microedition.io.file.FileConnection;


/**
 * JSR-234 implementation of ImageEffectProvider
 * @author Andreas Borglin
 */
final class JSR234Provider implements ImageConstants,ImageEffectProvider, MediaProcessorListener{

    // MediaProcessor
    private MediaProcessor mediaProc;
    
    // Controls
    private ImageEffectControl imageEffect;
    private ImageTonalityControl imageTonality;
    private ImageTransformControl imageTransform;
    private OverlayControl overlay;
    private WhiteBalanceControl whiteBalance;
    private ImageFormatControl imageFormat;
    //private EffectOrderControl effectOrder;
    
    // Internal usage
    private boolean calledTransform;
    private boolean calledEffect;
    private boolean calledOverlay;
    private boolean calledTonality;
    private boolean calledWhiteBalance;
    //private int overlayOrder;
    
    private String[] wbPresets;
    private String[] effectPresets;
    
    private int width;
    private int height;
    
    protected ImageSupport imageSupport;
     
    private OutputStream os;
    private InputStream is;
    private FileConnection fc;
    private byte[] imageData;
    private Vector oldData;
    private boolean undoMode;
     
    private ImageCreator imageCreator;
    
    protected JSR234Provider(boolean undoMode){
        this.undoMode = undoMode;
    }
    
    
    // PUBLIC API, IMAGEFFECTPROVIDER INTERFACE
    
    public void initialize(String format, String inputUrl, OutputStream os) throws Exception {
        
        fc = (FileConnection)Connector.open(inputUrl, Connector.READ);
        initialize(format,fc.openInputStream(),os);
        
    }
    
    /** Initialize the MediaProcessor and create controls */
    public void initialize(String format, InputStream is, OutputStream os) throws Exception {

        mediaProc = GlobalManager.createMediaProcessor(format);
        mediaProc.addMediaProcessorListener(this);

        this.os = os;
        this.is = is;
        mediaProc.setInput(is, (int) is.available());
        mediaProc.setOutput(os);

        setupControls();
            
        if(undoMode)
            oldData = new Vector();
    }
    
    public void setImageCreator(ImageCreator ic){
        imageCreator = ic;
    }
    
    public void close(){
        try{
            if(os != null){
                os.close();
                os = null;
            }
            if(is != null){
                is.close();
                is = null;
            }
            if(fc != null){
                fc.close();
                fc = null;
            }
            if(oldData != null){
                oldData.removeAllElements();
                oldData = null;
            }
            if(mediaProc != null){
                mediaProc.removeMediaProcessorListener(this);
                mediaProc.stop();
            }
        }
        catch(Exception e){}
        
    }
    
    public String[] getSupportedInputFormats(){
        return GlobalManager.getSupportedMediaProcessorInputTypes();
    }
    
    public String[] getSupportedOutputFormats(){
        return imageFormat.getSupportedFormats();
    }
    

    public void applyChanges() throws ImageEffectException {

        if(!anyChanges()){
            throw new ImageEffectException("No changes has been done");
        }
        
        try{

            // Workaround for weird JSR-234 bug where old width and height
            // sometimes is saved for future sessions..
            if(!imageTransform.isEnabled()){

                imageTransform.setSourceRect(0,0, width,height);
                imageTransform.setTargetSize(width, height, 0);
                imageTransform.setEnabled(true);

            }

            mediaProc.complete();

            if(fc != null){
                fc.close();
                is.close();
                is = null;
                fc = null;
            }
            
            if(imageData != null && undoMode){
                oldData.addElement(imageData);
            }

            // For preview mode, os will always be an ImageOutputStream
            // But for real mode, we might be writing directly to file..
            if(os instanceof ImageOutputStream){
                imageData = ((ImageOutputStream) os).getBuf();
                resetAndRealize();
            }

            
        }
        catch(Exception e){
            throw new ImageEffectException(e.toString());
        }
        
    }
    
    public void performOperation(int op, int a1, int a2, int a3, int a4) throws ImageEffectException{ // No var-args in 1.3 :-(

        try{
            switch(op){
            
                case ROTATE: rotate(a1); break;
                case FLIP: flip(a1); break;
                case RESIZE: resize(a1,a2); break;
                case CROP: crop(a1,a2,a3,a4); break;
                case FRAME: frame(a1,a2); break;
                case BRIGHTNESS: setBrightness(a1); break;
                case CONTRAST: setContrast(a1); break;
                case GAMMA: setGamma(a1); break;
                case WHITEBALANCE: setWhiteBalance(a1); break;
                case EFFECT: applyEffect(a1); break;
                default: break;
            
            }
        }
        catch(Exception e){
            throw new ImageEffectException(e.toString());
        }
    }
    
   
    public void setOutputFormat(String format) throws ImageEffectException{
        
        try{
            // If no format chosen, default to image/jpeg
            if(format == null)
                format = "image/jpeg";
            
            imageFormat.setFormat(format);
        }
        catch(Exception e){
            throw new ImageEffectException("Format " + format + " not supported");
        }
        
    }
  
    public void addOverlayImage(InputStream is, int x, int y, int order) throws ImageEffectException{ 
        
        if(overlay == null)
            throw new ImageEffectException("Overlay not supported");
        
        if(imageCreator == null)
            throw new ImageEffectException("ImageCreator not set!");
        
        try{
            // App UI must create Image since we don't know
            // what UI technology we are running under.
            Object image = imageCreator.createImage(is);
            if(image == null)
                throw new Exception("Image object must not be null");
            
            overlay.insertImage(image, x, y, order);
            overlay.setEnabled(true);
            calledOverlay = true;
        }
        catch(IllegalArgumentException e){
            throw new ImageEffectException("Missing support for eSWT image objects");
        }
        catch(Exception e){
            throw new ImageEffectException(e.toString());
        }

    }
    
    public void removeOverlayImage(int order) throws ImageEffectException{
        
        try{
            overlay.removeImage(overlay.getImage(order));
        }
        catch(Exception e){
            throw new ImageEffectException(e.toString());
        }
    }

    public void rotateAndScale(int rot, int newWidth, int newHeight) throws ImageEffectException {

        try{

            // bypass normal method of doing things..
            if(imageData != null && undoMode){
                oldData.addElement(imageData);
            }
            imageTransform.setSourceRect(0, 0, width, height);
            imageTransform.setTargetSize(0, 0, rot*90);
            imageTransform.setEnabled(true);

            mediaProc.complete();
            imageData = ((ImageOutputStream) os).getBuf();
            resetAndRealize();
            imageData = null;

            // now we can do it the normal way...
            resize(newWidth, newHeight);
            
        }
        catch(Exception e){
            throw new ImageEffectException(e.toString());
        }

    }
    
    public int getImageWidth(){
        return width;
    }
    
    public int getImageHeight(){
        return height;
    }
    
    public boolean canUndo(){
        return undoMode && oldData != null && oldData.size() > 0;
    }
    
    public boolean undo(){

        if (oldData == null || oldData.isEmpty() || !undoMode) {
            return false;
        }
        
        imageData = (byte[])oldData.elementAt(oldData.size()-1);
        oldData.removeElementAt(oldData.size()-1);

        try{
            resetAndRealize();
        }
        catch(Exception e){e.printStackTrace();}
        
        return true;
    }
    
    
    public boolean redo(){
        
        return false;
    }
    
    public void reset() throws ImageEffectException{

        try{
            resetAndRealize();
        }
        catch(Exception e){
            throw new ImageEffectException(e.toString());
        }
    }
    
    
    public byte[] getCurrentImageData(){
        return imageData;
    }
    
    
    // PRIVATE 
    
    
    private void setupControls(){

        // Get all controls. If some are not supported, we can detect that later
        imageEffect = (ImageEffectControl) mediaProc.getControl("javax.microedition.amms.control.imageeffect.ImageEffectControl");
        imageTonality = (ImageTonalityControl) mediaProc.getControl("javax.microedition.amms.control.imageeffect.ImageTonalityControl");
        imageTransform = (ImageTransformControl) mediaProc.getControl("javax.microedition.amms.control.imageeffect.ImageTransformControl");
        overlay = (OverlayControl) mediaProc.getControl("javax.microedition.amms.control.imageeffect.OverlayControl");
        whiteBalance = (WhiteBalanceControl) mediaProc.getControl("javax.microedition.amms.control.imageeffect.WhiteBalanceControl");
        imageFormat = (ImageFormatControl) mediaProc.getControl("javax.microedition.amms.control.ImageFormatControl");
        //effectOrder = (EffectOrderControl) mediaProc.getControl("javax.microedition.amms.control.EffectOrderControl");

        width = imageTransform.getSourceWidth();
        height = imageTransform.getSourceHeight();

        wbPresets = whiteBalance.getPresetNames();
        effectPresets = imageEffect.getPresetNames();

    }
    
    public ImageSupport getImageSupport(){
        
        ImageOperation[] operationSupport = new ImageOperation[OPERATIONS];
        
        // operations that conflict with each other
        int[] pixelConflicts = new int[]{EFFECT,WHITEBALANCE,BRIGHTNESS,CONTRAST,GAMMA,OVERLAY};
        int[] transformConflicts = new int[]{ROTATE,FLIP,CROP,FRAME,OVERLAY,RESIZE};
        
        if(imageEffect != null){
            operationSupport[EFFECT] = 
                    new ImageOperation(true,ImageOperation.VALUES_STRING,-1,-1,-1,-1,
                    "Apply image filter effects", imageEffect.getPresetNames(), pixelConflicts);
        }
        
        // Note: It is defined in the spec that the range is 0-100!
        if(imageTonality != null){
            int levels = imageTonality.getBrightnessLevels();
            operationSupport[BRIGHTNESS] = 
                    new ImageOperation((imageTonality.getContrastLevels() > 0),ImageOperation.VALUES_INTEGER,
                    0,100,levels,50,"Image brightness",null, pixelConflicts);
            levels = imageTonality.getContrastLevels();
            operationSupport[CONTRAST] = 
                    new ImageOperation((imageTonality.getContrastLevels() > 0),ImageOperation.VALUES_INTEGER,
                    0,100,levels,50,"Image contrast",null, pixelConflicts);
            levels = imageTonality.getGammaLevels();
            operationSupport[GAMMA] = 
                    new ImageOperation((imageTonality.getGammaLevels() > 0),ImageOperation.VALUES_INTEGER,
                    0,100,levels,50,"Image gamma", null, pixelConflicts);
        }
        
        if(imageTransform != null){
            operationSupport[ROTATE] =
                    new ImageOperation(true,ImageOperation.VALUES_INTEGER,0,270,90,0,"Image rotation in 90 degree multiples",
                    new String[]{"90", "180", "270"},transformConflicts);
            operationSupport[FLIP] = 
                    new ImageOperation(true,ImageOperation.VALUES_INTEGER,-1,-1,-1,-1,"Image flip horizontally/vertically",
                    new String[]{"Horizontal", "Vertical"},transformConflicts);
            operationSupport[RESIZE] = 
                    new ImageOperation(true,ImageOperation.VALUES_INTEGER, 0,-1,-1,-1,"Resize image width and height", null,transformConflicts);
            operationSupport[CROP] = 
                    new ImageOperation(true,ImageOperation.VALUES_INTEGER,-1,-1,-1,-1,"Crop image", null,transformConflicts);
            operationSupport[FRAME] = 
                    new ImageOperation(true,ImageOperation.VALUES_INTEGER,-1,-1,-1,-1,"Create frame around image", null,transformConflicts);
        }
        
        if(whiteBalance != null){
            operationSupport[WHITEBALANCE] = 
                    new ImageOperation(true,ImageOperation.VALUES_STRING,whiteBalance.getMinColorTemp(),whiteBalance.getMaxColorTemp(),
                    whiteBalance.getNumberOfSteps(),whiteBalance.getColorTemp(),"Change image whitebalance", whiteBalance.getPresetNames(), pixelConflicts);
        }
        
        if(overlay != null){
            operationSupport[OVERLAY] = 
                    new ImageOperation(true,ImageOperation.VALUES_NONE,-1,-1,-1,-1,"Add overlay images",null, pixelConflicts);
        }
        
        imageSupport = new ImageSupport(operationSupport);
        
        return imageSupport;
    }
    
    /** Reset MediaProcessor and set new input and output */
    private void resetAndRealize() throws Exception {

        os.close();
        
        mediaProc.setInput(new ByteArrayInputStream(imageData), imageData.length);
        os = new ImageOutputStream();
        mediaProc.setOutput(os);
        
        setupControls(); // Is this really needed?

        imageEffect.setEnabled(false);
        imageTonality.setEnabled(false);
        imageTransform.setEnabled(false);
        overlay.setEnabled(false);
        whiteBalance.setEnabled(false);

        calledTransform = false;
        calledEffect = false;
        calledOverlay = false;
        calledWhiteBalance = false;
        calledTonality = false;
        //overlayOrder = 0;

        
    }
    

    private void resize(int neww, int newh) throws Exception {
        transform(0,0,width,height,neww,newh,0);
    }
    
    private void rotate(int rotate) throws Exception{
        
        // value we get is a predefined value from ImageConstants, so multiply by 90
        rotate *= 90;
        
        transform(0, 0,width,height, 0, 0, rotate);
        
        // If we rotate, then we must switch w and h
        if(rotate == 90 || rotate == 270){
            int temp = width;
            width = height;
            height = temp;
        }
        
    }
    
    private void flip(int direction) throws Exception {
    
        int xx, yy, w, h;
        xx = yy = w = h = 0;
        
        switch (direction) {

            case FLIP_BOTH: {
                xx = width;
                yy = height; // 0
                w = -width;
                h = -height; // height
                break;
            }
            case FLIP_HORIZONTALLY: {
                xx = width;
                yy = 0; // 0
                w = -width;
                h = height; // height
                break;
            }
            case FLIP_VERTICALLY: {
                xx = 0;
                yy = height;
                w = width;
                h = -height;
                break;
            }
        }
        transform(xx, yy, w, h, 0, 0, 0);
        
    }
    
    private void crop(int x, int y, int w, int h) throws Exception {
        transform(x, y, w, h, w, h, 0);
    }
    
    private void frame(int fw, int fh) throws Exception {
        
        int neww = width + (fw * 2);
        int newh = height + (fh * 2);
        
        transform(-fw, -fh, neww, newh,width,height, 0);

    }
    
    private void setBrightness(int value) throws Exception {
        
        if(imageTonality == null || imageTonality.getBrightnessLevels() < 1){
            throw new ImageEffectException("Brightness not supported");
        }
        
        if(calledTonality){
            applyChanges();
        }
        
        imageTonality.setBrightness(value);
        imageTonality.setEnabled(true);
        calledTonality = true;
        
    }
    
    private void setContrast(int value) throws Exception {
        
        if(imageTonality == null || imageTonality.getContrastLevels() < 1){
            throw new ImageEffectException("Contrast not supported");
        }
        
        if(calledTonality){
            applyChanges();
        }
        
        imageTonality.setContrast(value);
        imageTonality.setEnabled(true);
        calledTonality = true;
        
    }
    
    private void setGamma(int value) throws Exception {
        
        if(imageTonality == null || imageTonality.getGammaLevels() < 1){
            throw new ImageEffectException("Gamma not supported");
        }
        
        imageTonality.setGamma(value);
        imageTonality.setEnabled(true);
        calledTonality = true;
    }
    
    private void setWhiteBalance(int index) throws Exception {
        
        if(whiteBalance == null)
            throw new ImageEffectException("White balance not supported");
        
        if(calledWhiteBalance){
            applyChanges();
        }
        
        whiteBalance.setPreset(wbPresets[index]);
        whiteBalance.setEnabled(true);
        calledWhiteBalance = true;
        
    }
    
    private void applyEffect(int index) throws Exception {
        
        if(imageEffect == null)
            throw new ImageEffectException("Effects not supported.");
        
        if(calledEffect){
            applyChanges();
        }
        
        imageEffect.setPreset(effectPresets[index]);
        imageEffect.setEnabled(true);
        calledEffect = true;
        
    }
    
     private void transform(int x, int y, int sw, int sh, int nw, int nh, int rot) throws Exception {
        
         if(imageTransform == null)
             throw new ImageEffectException("Operation not supported.");
         
        // If transform has already been called for this session
        if(calledTransform){
            applyChanges();
        }
        
        imageTransform.setSourceRect(x, y, sw, sh);
        imageTransform.setTargetSize(nw, nh, rot);
        imageTransform.setEnabled(true);
        calledTransform = true;
        
    }
        
     public void scaleAndRotate(int w, int h, int rot) throws Exception{
         transform(0,0,width,height,w,h,rot);
     }
     
     private boolean anyChanges(){

        return calledEffect || calledTransform ||
                calledTonality || calledOverlay ||
                calledWhiteBalance;
        
    }
     
    public void mediaProcessorUpdate(MediaProcessor mp, String event, Object edata) {

    }
    
}
